// **********************************************************************
//
// <copyright>
// 
//  BBN Technologies
//  10 Moulton Street
//  Cambridge, MA 02138
//  (617) 873-8000
// 
//  Copyright (C) BBNT Solutions LLC. All rights reserved.
// 
// </copyright>
// **********************************************************************
// 
// $Source: /cvs/distapps/openmap/src/openmap/com/bbn/openmap/layer/rpf/MakeToc.java,v $
// $RCSfile: MakeToc.java,v $
// $Revision: 1.14 $
// $Date: 2006/08/17 15:19:06 $
// $Author: dietrick $
// 
// **********************************************************************

/*
 * The meat of this code is based on source code provided by The MITRE
 * Corporation, through the browse application source code.  Many
 * thanks to Nancy Markuson who provided BBN with the software, and to
 * Theron Tock, who wrote the software, and Daniel Scholten, who
 * revised it - (c) 1994 The MITRE Corporation for those parts, and
 * used/distributed with permission. 
 */
package com.bbn.openmap.layer.rpf;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.util.Vector;

import com.bbn.openmap.event.ProgressEvent;
import com.bbn.openmap.event.ProgressListener;
import com.bbn.openmap.event.ProgressSupport;
import com.bbn.openmap.io.BinaryBufferedFile;
import com.bbn.openmap.io.BinaryFile;
import com.bbn.openmap.proj.coords.DMSLatLonPoint;
import com.bbn.openmap.proj.coords.LatLonPoint;
import com.bbn.openmap.util.ArgParser;
import com.bbn.openmap.util.Debug;

/**
 * This is a class that will generate A.TOC files that the RpfLayer requires.
 * A.TOC files provide the RpfLayer with an idea of what data is available to
 * it, its geographic coverage, and chart type. With the A.TOC contents, the
 * RpfLayer is able to find which frames are appropriate for a given projection
 * location. It is very important to have a valid A.TOC directory.
 * <P>
 * 
 * The RPF specification, MIL-STD-2411, has definitions for how frames are to be
 * laid out and found within a RPF directory. All RPF data is supposed to lie
 * under one RPF directory, and an A.TOC file, describing all the files and
 * their groupings, should be directly within the RPF directory. That's why the
 * RpfLayer needs a path to a RPF directory - it's really looking for the A.TOC
 * file, and knows where to find it. It also needs a path to the RPF directory
 * because it needs to prepend that path to the paths to the files that the
 * A.TOC file knows about.
 * <P>
 * 
 * The A.TOC files that can be created with this MakeToc class can be created to
 * contain absolute frame paths. The MakeToc class can take the paths to several
 * RPF directories, and create a single A.TOC file that preserves all of their
 * current file paths. You have to use a lot of caution with this capability,
 * however. These A.TOCs containing absolute file paths will not work if the
 * data is moved to another machine, or if referenced by a machine with a
 * different type file system (i.e. Windows). They may not work for other
 * implementations of code that display RPF data - the code in this package has
 * been modified to test for absolute file names.
 * <P>
 * 
 * That said, absolute file names should be used instead of giving the RpfLayer
 * several RPF directories. The RpfTocHandler does much less work when it is
 * allowed to group coverages together to make bigger areas.
 * <P>
 * 
 * This code was ported from C code provided in the original Mitre RPF package
 * that had limits to the number of frames that could make up the areas. I'll be
 * working to eliminate those limits, but I wanted to get a working version of
 * the code out there. I'm also planning on modifying this class so that it can
 * load the RpfTocHandler directly, therefore eliminating the need for A.TOCs
 * altogether when there is more than one RPF directory.
 * <P>
 * 
 * <pre>
 * 
 * 
 *   Usage:  java com.bbn.openmap.layer.rpf.MakeToc (RPF dir path) (RPF dir path) ...
 * 
 * 
 * </pre>
 * 
 * This will create an A.TOC file in the current directory for the RPF files in
 * the RPF directory paths. Use:
 * 
 * <pre>
 * 
 * 
 *   java com.bbn.openmap.layer.rpf.MakeToc -help
 * 
 * 
 * </pre>
 * 
 * for other options.
 * 
 * <P>
 * NOTE: Make sure that the RPF directories and their contents are in upper
 * case. It's a spec requirement, although with CD copies and FTP downloads, the
 * file name cases sometimes get switched. Use
 * com.bbn.openmap.layer.rpf.ChangeCase to modify the file name cases. Also, if
 * there is more than one RPF directory in the path to the image frames, use the
 * absolute path option. Otherwise, the code will focus on making the top-most
 * RPF directory the one to key the internal relative paths off of, and that
 * might not be what you want.
 * </P>
 * 
 * @see com.bbn.openmap.util.wanderer.ChangeCase
 */
public class MakeToc {

	/**
	 * According to Dan Scholten's original code, this was 2 times the max -
	 * changed from 30 on 6/17/94 to 200 for 81 JNC's in zone 1. This might not
	 * be enough for world-wide coverage of larger scale maps that are now
	 * available. This number may have to be increased depending on how much
	 * data you need.
	 */
	public final static int DEFAULT_MAX_SIDE = 200;

	public final static double EPS = 0.01;
	public final static double EPS2 = 0.0001;
	/** Output file name of the A.TOC file. */
	public final static String ATOC_FILENAME = "A.TOC";
	/** The boundary edge frame length for groups. */
	protected int maxSide = DEFAULT_MAX_SIDE;
	/** Flag to use relative frames paths - default is true. */
	protected boolean relativeFramePaths = true;
	/** The producer name for the frame files. Default is DMAAC. */
	protected String producer = "DMAAC";

	protected ProgressSupport progressSupport;

	/** An internal representation of a Frame file. */
	public class Frame {
		double left;
		double right;
		double top;
		double bottom;
		/* New DKS: for computing GEOREF #'s over polar region */
		double swlat;
		double swlon;
		double h_interval;
		double v_interval;
		double h_resolution;
		double v_resolution;
		String scale; // length 12
		char zone;
		boolean marked;
		int group;
		int x;
		int y;
		String filename;
		boolean cib;
		boolean cdted;

		public double EPS() {
			return (Math.abs(right - left) * MakeToc.EPS);
		}

		public String toString() {
			StringBuffer s = new StringBuffer();
			s.append("Frame - ").append(filename).append("\n");
			s.append("  zone = ").append(zone).append("\n");
			s.append("  marked = ").append(marked).append("\n");
			s.append("  scale = ").append(scale).append("\n");
			s.append("  group = ").append(group).append("\n");

			if (Debug.debugging("maketocframe")) {
				s.append("  top = ").append(top).append("\n");
				s.append("  bottom = ").append(bottom).append("\n");
				s.append("  left = ").append(left).append("\n");
				s.append("  right = ").append(right).append("\n");
				s.append("  h_interval = ").append(h_interval).append("\n");
				s.append("  v_interval = ").append(v_interval).append("\n");
				s.append("  h_resolution = ").append(h_resolution).append("\n");
				s.append("  v_resolution = ").append(v_resolution).append("\n");
			}

			return s.toString();
		}
	}

	/** An internal representation of a boundary rectangle for frames. */
	public class Group {
		double[] horiz_pos;
		double[] vert_pos;
		int left;
		int right;
		int top;
		int bottom;
		String scale;
		char zone;
		double h_interval;
		double v_interval;
		double h_resolution;
		double v_resolution;
		boolean cib;
		boolean cdted;

		public Group() {
			horiz_pos = new double[maxSide];
			vert_pos = new double[maxSide];
		}

		public String toString() {
			StringBuffer s = new StringBuffer();
			s.append("Group - \n");
			s.append("  zone = ").append(zone).append("\n");
			s.append("  scale = ").append(scale).append("\n");
			s.append("  left = ").append(left).append("\n");
			s.append("  right = ").append(right).append("\n");
			s.append("  top = ").append(top).append("\n");
			s.append("  bottom = ").append(bottom).append("\n");
			s.append("  is cdted = ").append(cdted).append("\n");
			s.append("  is cib = ").append(cib).append("\n");
			return s.toString();
		}
	}

	public MakeToc() {
		progressSupport = new ProgressSupport(this);
	}

	/**
	 * Create an A.TOC file.
	 * 
	 * @param argv
	 *            The arguments should at least include a path to a RPF file
	 *            root directory. Other options can be found by using a -help
	 *            option.
	 */
	public static void main(String[] argv) {
		Debug.init();
		boolean Dchum = false;

		ArgParser ap = new ArgParser("MakeToc");
		ap.add("absolute", "Use absolute paths in A.TOC - Use for multiple RPF Directories");
		ap.add("boundary", "Maximum frames on a boundary edge (Default 200)", 1);
		ap.add("dchum", "DCHUM files are included.");
		ap.add("log", "Pathname of log file to list A.TOC creation output.", 1);
		ap.add("output", "Path to directory to place A.TOC file. (Default is current directory)", 1);
		ap.add("producer", "The producer of the frames (Default DMAAC).  Five letter code.", 1);
		ap.add("verbose", "Print out progress");
		ap.add("extraverbose", "Print out ALL progress");
		ap.add("nw", "Don't put up swing progress window (Use this if you are getting weird exceptions)");
		ap.add("paths",
				"Space separated paths to RPF directory or directories.  Should be last.  If more than one directory is listed, then absolute paths are used in the A.TOC file.",
				ArgParser.TO_END);

		if (!ap.parse(argv)) {
			ap.printUsage();
			System.exit(0);
		}

		String outputFile = "." + File.separator + RpfTocHandler.RPF_TOC_FILE_NAME;

		String arg[];
		arg = ap.getArgValues("output");
		if (arg != null) {
			outputFile = arg[0] + File.separator + RpfTocHandler.RPF_TOC_FILE_NAME;
		}

		arg = ap.getArgValues("log");
		if (arg != null) {
			String logfile = arg[0];
			Debug.directOutput(logfile, false, true);
			Debug.output("MakeToc: Creating log at " + logfile + " at " + java.util.Calendar.getInstance().getTime());
		}

		arg = ap.getArgValues("dchum");
		if (arg != null) {
			Dchum = true;
		}

		arg = ap.getArgValues("verbose");
		if (arg != null) {
			Debug.put("maketoc");
		}

		arg = ap.getArgValues("extraverbose");
		if (arg != null) {
			Debug.put("maketoc");
			Debug.put("maketocdetail");
		}

		String[] paths = null;
		arg = ap.getArgValues("paths");
		if (arg != null) {
			paths = arg;
		} else {
			paths = ap.getRest();
		}

		if (paths == null || paths.length == 0) {
			Debug.output("MakeToc: need a path to start searching for RPF frames.");
			System.exit(0);
		}

		MakeToc mt = new MakeToc();

		// If the -nw argument was not used, add a progress gauge.
		arg = ap.getArgValues("nw");
		if (arg == null) {
			try {
				mt.addProgressListener(new com.bbn.openmap.gui.ProgressListenerGauge("RPF A.TOC File Creation"));
			} catch (RuntimeException re) {

			}
		}

		boolean argFlagged = false;
		arg = ap.getArgValues("absolute");
		if (arg != null) {
			argFlagged = true;
		}

		arg = ap.getArgValues("producer");
		if (arg != null) {
			mt.setProducer(arg[0]);
		}

		if ((paths != null && paths.length > 1) || argFlagged) {
			Debug.output("MakeToc:  creating A.TOC with absolute path names.");
			mt.setRelativeFramePaths(false);
		}

		arg = ap.getArgValues("boundary");
		int max_side = DEFAULT_MAX_SIDE;
		if (arg != null) {
			try {
				max_side = Integer.parseInt(arg[0]);
				if (max_side <= DEFAULT_MAX_SIDE) {
					Debug.output("MakeToc: Boundary number specified (" + max_side
							+ ") is too small.  Using default of 200.");
					max_side = DEFAULT_MAX_SIDE;
				}
			} catch (NumberFormatException nfe) {
				Debug.output("MakeToc: Tried to pass a bogus integer (" + arg[0]
						+ ") as a boundary limit.  Using default of 200.");
				max_side = DEFAULT_MAX_SIDE;
			}
		}
		mt.setMaxSide(max_side);
		mt.fireProgressUpdate(ProgressEvent.START, "Searching for RPF frames", 0, 100);

		paths = mt.searchForRpfFiles(paths);

		try {

			mt.create(paths, outputFile, Dchum);

		} catch (MakeTocException mte) {
			Debug.error("Problem creating A.TOC file: \n" + mte.getMessage());
		}

		System.exit(0);

	}

	/**
	 * Create a A.TOC file specificed by the frame file list, at the location
	 * specified.
	 * 
	 * @param rpfFilePaths
	 *            An array of all RPF Frame file paths. If these paths are
	 *            relative, the MakeToc class should be set for that.
	 * @param outputFile
	 *            the complete pathname to an A.TOC file to be written.
	 * @exception MakeTocException
	 *                if anything goes wrong.
	 */
	public void create(String[] rpfFilePaths, String outputFile) throws MakeTocException {
		create(rpfFilePaths, outputFile, false);
	}

	/**
	 * Create a A.TOC file specificed by the frame file list, at the location
	 * specified.
	 * 
	 * @param rpfFilePaths
	 *            An array of all RPF Frame file paths. If these paths are
	 *            relative, the MakeToc class should be set for that.
	 * @param outputFile
	 *            the complete pathname to an A.TOC file to be written.
	 * @param dchum
	 *            If dchum is present, all frames get placed in their own group.
	 *            False is default. Dchum are replacement subframes.
	 * @exception MakeTocException
	 *                if anything goes wrong.
	 */
	public void create(String[] rpfFilePaths, String outputFile, boolean dchum) throws MakeTocException {

		RpfHeader head = new RpfHeader();
		Vector<Frame> frames = new Vector<Frame>(rpfFilePaths.length);
		Vector<Group> groups = new Vector<Group>();

		fireProgressUpdate(ProgressEvent.UPDATE, "Organizing frames", 0, 100);

		organizeFrames(rpfFilePaths, head, frames);

		if (head.standardNumber == null) {
			throw new MakeTocException("MakeToc: No RPF frames found.");
		}

		groupFrames(frames, groups, dchum);
		fireProgressUpdate(ProgressEvent.UPDATE, "Writing A.TOC file", 100, 100);
		writeTOCFile(outputFile, head, frames, groups);
		fireProgressUpdate(ProgressEvent.DONE, "A.TOC file complete", 100, 100);
	}

	/**
	 * Look for RPF frame files, given a bunch of places to start looking. The
	 * output of this can be passed to the create method.
	 * 
	 * @param startDirs
	 *            Directory paths.
	 * @return an array of strings representing path names to RPF frame files.
	 */
	public String[] searchForRpfFiles(String[] startDirs) {
		RpfFileSearch search = new RpfFileSearch();
		for (int i = 0; i < startDirs.length; i++) {
			search.handleEntry(startDirs[i]);
		}
		return search.getFiles();
	}

	/**
	 * Set whether to use relative frame paths in the A.TOC file.
	 */
	public void setRelativeFramePaths(boolean setting) {
		relativeFramePaths = setting;
	}

	public boolean getRelativeFramePaths() {
		return relativeFramePaths;
	}

	/**
	 * Set the 5 letter producer code for the frames. If you didn't make the
	 * frames, they DMA probably did, so the default is applicable - DMAAC.
	 * There are a bunch of accepted codes in the MIL-STD-2411 for producers.
	 */
	public void setProducer(String setting) {
		if (setting.length() != 5) {
			if (setting.length() >= 5) {
				producer = setting.substring(0, 5);
			} else {
				producer = setting + createPadding(5 - setting.length(), false);
			}
		} else {
			producer = setting;
		}
	}

	/** Get the producer code currently set. */
	public String getProducer() {
		return producer;
	}

	/**
	 * Set the Maximum number of frames along a group boundary edge. Don't
	 * change this after starting to group the frames.
	 */
	protected void setMaxSide(int set) {
		maxSide = set;
	}

	/**
	 * Get the Maximum number of frames along a group boundary edge.
	 */
	protected int getMaxSide() {
		return maxSide;
	}

	/** A little function to tell of one edge is near another. */
	protected boolean near(double a, double b, double eps) {
		return (Math.abs(a - b) < eps); /* EPS was 0.0001 */
	}

	/**
	 * Get all the frame paths, and sort through them. This method sets up the
	 * frames vector and loads each Frame with it's attributes, so it can be
	 * grouped with its neighbors.
	 * 
	 * @param framePaths
	 *            the array of RPF file paths.
	 * @param head
	 *            an RpfHeader object to load with production information, that
	 *            will be put into the A.TOC file.
	 * @param frames
	 *            the frame vector to load.
	 */
	public void organizeFrames(String[] framePaths, RpfHeader head, Vector<Frame> frames) {

		int tail;
		int i;

		/* New, DKS */
		// boolean Cib = false; /* CIB data flag: 1:I1(10M); 2:I2(5M) */
		// boolean Cdted = false; /* CDTED data flag: 1: DT1(100M) */
		boolean isoverview = false;
		boolean islegend = false;

		Frame frame;

		RpfFileSections.RpfCoverageSection coverage;

		Debug.message("maketoc", "MakeToc.organizeFrames: *** initial look at frames ***");

		/* # of frames = # of pathname records = #files */
		int nFrames = framePaths.length;

		if (Debug.debugging("maketoc")) {
			Debug.output("Number of frames: " + nFrames);
		}

		/* for each frame file */
		for (i = 0; i < nFrames; i++) {
			isoverview = false;
			islegend = false;

			String framePath = framePaths[i];

			if (Debug.debugging("maketoc")) {
				Debug.output("MakeToc: frame number " + i + ", " + framePath);
			}

			try {
				BinaryFile binFile = new BinaryBufferedFile(framePath);

				// Frame file names are 8.3 notation, might want to
				// check
				// that here, to blow off dummy files.
				String fn = binFile.getName();
				if (fn.length() != 12) {
					// Not a RPF Frame file
					if (Debug.debugging("maketoc")) {
						Debug.error("MakeToc: " + framePath + " is not a RPF image file - ignoring");
					}
					continue;
				}

				RpfFileSections rfs = new RpfFileSections();

				binFile.seek(0);

				if (!head.read(binFile)) {
					// Not a RPF Frame file
					if (Debug.debugging("maketoc")) {
						Debug.error("MakeToc: " + framePath + " is not a RPF image file - ignoring");
					}
					continue;
				}

				binFile.seek(head.locationSectionLocation);

				rfs.parse(binFile);
				coverage = rfs.parseCoverageSection(binFile);

				if (coverage == null) {
					Debug.error(
							"MakeToc: error reading coverage section for " + framePath + ", (file " + i + ") skipping");

					binFile.close();
					continue;
				}

				if (Debug.debugging("maketocframedetail")) {
					Debug.output("MakeToc.organizeFrames: coverage section for " + framePath + ", " + coverage);
				}

				binFile.close();
				binFile = null;

			} catch (FileNotFoundException e) {
				Debug.error("MakeToc: " + framePath + " not found, being ignored.");
				continue;
			} catch (IOException ioe) {
				Debug.error("MakeToc: File IO Error during read of: " + framePath + "! Being ignored. \n" + ioe);
				continue;
			}

			frame = new Frame();
			frames.add(frame);

			frame.filename = framePath;

			// This will be the actual file name, without parental
			// path.
			String framename;

			tail = frame.filename.lastIndexOf(File.separatorChar);
			if (tail == -1) {
				framename = frame.filename;
			} else {
				framename = frame.filename.substring(++tail);
			}

			if (framename.length() != 12) {
				Debug.error("filename must be 12 chars long - " + framename);
				return;
			}

			// 9 is the character after the period.
			isoverview = (framename.charAt(9) == 'O');
			if (!isoverview) {
				islegend = framename.regionMatches(true, 9, "LG", 0, 2);
			}

			// Check and see of the file thinks it's name is the same
			// as it actually is. If they differ, rule in favor of
			// what the frame thinks it is.

			// Let's just be passive here, and name it to whatever it
			// is. If we found the frame, then we'll find it later,
			// too. -DFD

			// if (!framename.equals(head.filename)) { /* DKS */
			// File file = new File(frame.filename);
			// File newFile = new File(frame.filename.substring(0,
			// tail),
			// head.filename);
			// file.renameTo(newFile);
			// framename = head.filename;

			// Debug.output("WARNING: File \"" + framename +
			// "\" doesn't match internal name \"" + head.filename +
			// "\" - Fixed.");
			// }

			isoverview = false;
			islegend = false;
			String padding = null;
			String seriesCode = head.filename.substring(9, 11);
			RpfProductInfo rpi = RpfProductInfo.get(seriesCode);

			if (rpi == RpfProductInfo.UK) {

				String dblChkSeriesCode = framename.substring(9, 11);
				RpfProductInfo rpi2 = rpi;

				if (!seriesCode.equals(dblChkSeriesCode)) {
					rpi2 = RpfProductInfo.get(dblChkSeriesCode);
				}

				if (rpi2 == RpfProductInfo.UK) {

					Debug.output("MakeToc: " + frame.filename + " / " + head.filename
							+ " (filename/header) unknown map type " + seriesCode + " / " + dblChkSeriesCode
							+ " - ignoring.");
					frames.remove(frame);
					continue;
				}
			}

			String scaleString = rpi.scaleString;
			if (rpi.scale == RpfConstants.Various || scaleString == null || scaleString.length() == 0) {
				// need to figure out how to consult the frame for
				// what it is.
				// RpfAttributes.chartSeriesCode might have something
				// to base it off.
				// GNC = GN, JNC = JN, ONC = ON, TPC = TP, JOG = 15,
				// TLM50 = V7,
				// But I'm not sure about the others. For now, prompt
				// for scale.

				// Try something new, look it up if it exists.
				scaleString = searchForATOCForScaleReference(framePath);
				if (scaleString == null || scaleString.length() == 0) {
					scaleString = promptForScale(
							"What is the scale for " + frame.filename + "? (Answer should look like: 1:XXX,XXX)");
					if (scaleString == null || scaleString.length() == 0) {
						Debug.error("Bad input for scale for " + frame.filename + ", skipping.");
						frames.remove(frame);
						continue;
					}
				}
			}

			if (rpi.dataType.equalsIgnoreCase(RpfConstants.CIB)) {
				frame.cib = true;
			} else if (rpi.dataType.equalsIgnoreCase(RpfConstants.CDTED)) {
				frame.cdted = true;
			} // else do nothing for CADRG

			// Set the string to length 12, was 15 for some reason.
			int scaleStringLength = 12;
			if (scaleString.length() < scaleStringLength) {
				padding = createPadding(scaleStringLength - scaleString.length(), false);
				scaleString += padding;
			} else if (scaleString.length() > scaleStringLength) {
				scaleString = scaleString.substring(0, scaleStringLength);
			}

			frame.scale = scaleString;
			frame.zone = head.filename.charAt(11);

			if (isoverview) {
				coverage.nwlat = coverage.nelat = coverage.nwlon = coverage.swlon = coverage.swlat = coverage.selat = coverage.nelon = coverage.selon = 0;
				coverage.latInterval = coverage.lonInterval = coverage.nsVertRes = coverage.ewHorRes = 0;
			}

			if (islegend) {
				coverage.nwlat = coverage.nelat = coverage.nwlon = coverage.swlon = coverage.swlat = coverage.selat = coverage.nelon = coverage.selon = 0;
				coverage.latInterval = coverage.lonInterval = coverage.nsVertRes = coverage.ewHorRes = 0;
			}

			/*
			 * PBF 6-18-94 check for rectangular coverage or polar frame
			 */
			if (frame.zone == '9' || frame.zone == 'J') {
				/*
				 * Polar. Convert boundary from lat-long degrees to pixels
				 */
				/* DKS 1/95: North pole: "9" code */
				if (frame.zone == '9') {
					if (Debug.debugging("maketoc"))
						Debug.output("Processing NORTH pole");

					frame.left = (90.0 - coverage.nwlat) * Math.sin(coverage.nwlon * Math.PI / 180.0)
							/ coverage.latInterval;

					frame.right = (90.0 - coverage.selat) * Math.sin(coverage.selon * Math.PI / 180.0)
							/ coverage.latInterval;

					frame.top = -1 * (90.0 - coverage.nwlat) * Math.cos(coverage.nwlon * Math.PI / 180.0)
							/ coverage.latInterval;

					frame.bottom = -1 * (90.0 - coverage.selat) * Math.cos(coverage.selon * Math.PI / 180.0)
							/ coverage.latInterval;
				} else { /* DKS 1/95: South pole: "J" code */
					if (Debug.debugging("maketoc"))
						Debug.output("Processing SOUTH pole");

					frame.left = (90.0 + coverage.nwlat) * Math.sin(coverage.nwlon * Math.PI / 180.0)
							/ coverage.latInterval;

					frame.right = (90.0 + coverage.selat) * Math.sin(coverage.selon * Math.PI / 180.0)
							/ coverage.latInterval;

					frame.top = (90.0 + coverage.nwlat) * Math.cos(coverage.nwlon * Math.PI / 180.0)
							/ coverage.latInterval;

					frame.bottom = (90.0 + coverage.selat) * Math.cos(coverage.selon * Math.PI / 180.0)
							/ coverage.latInterval;
				} /* if South pole */

				/* DKS 8/1/94: Added for GEOREF calc later */
				frame.swlat = coverage.swlat;
				frame.swlon = coverage.swlon;

				if (Debug.debugging("maketoc")) {
					Debug.output("MakeToc: " + frame.filename + " is a Polar frame");
				} /* if Debug.debugging("maketoc") */

			} else {

				frame.left = coverage.nwlon;
				frame.right = coverage.selon;

				/*
				 * NEW, DKS 6/94. Correct for frame straddling 180 deg.
				 */
				if (coverage.selon < coverage.nwlon) {
					frame.right = 180.0;
				}

				frame.top = coverage.nwlat;
				frame.bottom = coverage.selat;
			}

			frame.h_interval = coverage.lonInterval;
			frame.v_interval = coverage.latInterval;
			frame.h_resolution = coverage.ewHorRes;
			frame.v_resolution = coverage.nsVertRes;

			frame.marked = false;

			if (Debug.debugging("maketocframedetail")) {
				Debug.output("MakeToc: nw_lon = " + coverage.nwlon + ", se_lon = " + coverage.selon
						+ "\n         nwlat = " + coverage.nwlat + ", selat = " + coverage.selat + "\n    NEW: swlat = "
						+ coverage.swlat + ", swlon = " + coverage.swlon + "\n         vert_interval = "
						+ coverage.latInterval + ", horiz_interval = " + coverage.lonInterval
						+ "\n         vertical resolution = " + coverage.nsVertRes + ", horizontal resolution = "
						+ coverage.ewHorRes + "\n         left = " + frame.left + ", right = " + frame.right
						+ "\n         top = " + frame.top + ", bottom = " + frame.bottom + "\n");

			}
		} /* for i: each input frame file */
	}

	protected String searchForATOCForScaleReference(String framePath) {
		// System.out.println("looking for scale for " + framePath);
		RpfTocHandler tocHandler = new RpfTocHandler();
		File frameFile = new File(framePath);
		File parent = frameFile.getParentFile();
		while (parent.exists()) {

			// System.out.println("checking " + parent + " for A.TOC");
			File tocFile = tocHandler.getTocFile(parent.getAbsolutePath());

			if (tocFile != null && tocHandler.loadFile(parent.getAbsolutePath())) {
				// System.out.println("found it at :" + parent);
				tocHandler.valid = true;
				break;
			}

			parent = parent.getParentFile();
		}

		if (tocHandler.valid) {
			int pathLength = framePath.length();
			String frameChartCode = framePath.substring(pathLength - 3, pathLength - 1);
			int frameZone = Integer.parseInt(new Character(framePath.charAt(pathLength - 1)).toString());
			for (RpfTocEntry entry : tocHandler.entries) {
				String chartCode = entry.coverage.chartCode;
				int zone = entry.coverage.zone;

				if (chartCode.equalsIgnoreCase(frameChartCode) && zone == frameZone) {
					// System.out.println("Found matching chart code: " +
					// frameChartCode + ", zone: " + frameZone);
					// Lets look for the frame file in this entry
					tocHandler.loadFrameInformation(entry);
					for (RpfFrameEntry[] entryDim : entry.frames) {
						for (RpfFrameEntry fEntry : entryDim) {
							// System.out.println("comparing " + framePath + "
							// to " + fEntry.framePath);
							if (fEntry.framePath.endsWith(framePath)) {
								System.out.println("setting " + entry.scale + " for " + framePath);
								return entry.scale;
							}
						}
					}
				}
			}
		}
		return null;
	}

	/**
	 * Prompt for input.
	 */
	protected String promptForScale(String query) {
		try {
			String answer = null;
			System.out.println(query);
			InputStreamReader isr = new InputStreamReader(System.in);
			BufferedReader bufr = new BufferedReader(isr);
			answer = bufr.readLine();
			return answer;
		} catch (IOException ioe) {
			Debug.error("MakeToc: IOException trying to get an answer from you.  Dang.");
			return null;
		}
	}

	/**
	 * Create and write out an A.TOC file.
	 * 
	 * @param filename
	 *            the output filename.
	 * @param head
	 *            the RpfHeader containing header information.
	 * @param frames
	 *            the frame Vector.
	 * @param groups
	 *            the file groups Vector.
	 */
	protected void writeTOCFile(String filename, RpfHeader head, Vector<Frame> frames, Vector<Group> groups)
			throws MakeTocException {

		short us;
		int i, j, tail;

		/*
		 * DKS changed from left, right for polar zone: new left_bottom longit.
		 */
		double left_b, left_t, right_b, right_t, top, bottom;
		double xleft, xright, ytop, ybottom;

		/* !! To be filled in later */
		int TOC_Nitf_hdr_size = 0; /*
									 * ?? Nitf header size for output TOC
									 */
		int Loc_sec_len; /* Location section length */
		int Bound_tbl_len; /* Boundary rectangle table length */
		int Frame_hdr_len = 13; /* Frame index header length */
		int Frame_index_rec_len = 33; /*
										 * Frame index record length (was 37)
										 */
		int Frame_sec_len; /* Frame section length */

		RandomAccessFile fout = null;
		int groupCount = groups.size();
		int nFrames = frames.size();

		/* cumulative pathname positions */
		int[] pathname_pos = new int[nFrames];

		/* List of pathnames: directories */
		String[] direct = new String[nFrames];

		/* Allocations for uniq directories */
		int[] uniq_dir_ptr = new int[nFrames]; /*
												 * index from filename to uniq
												 * direct.
												 */
		int[] uniq_dir_pos = new int[nFrames]; /*
												 * position of direct. name in
												 * file
												 */

		/* list of direct. names */
		String[] uniq_dir = new String[nFrames];

		String georef = "AAAAAA"; /* GEOREF # */
		Frame frame;
		Group group;

		// Right now, just write the new file locally.
		try {
			fout = new RandomAccessFile(filename, "rw");

			/* WRITE TOC : */
			if (Debug.debugging("maketoc")) {
				Debug.output("MakeToc: *** writing TOC ***\n  at: " + filename);
			}

			/* HEADER SECTION */
			if (Debug.debugging("maketoc")) {
				Debug.output("MakeToc: *** writing header section ***");
			}

			String charString;
			char[] nt = new char[1];
			nt[0] = '\0';

			/* DKS. Can't write structure because of pad bytes */
			/* fwrite(&head, sizeof(head), 1, fout); */

			fout.writeBoolean(head.endian); // Big Endian - should
			// match head.endian
			fout.writeShort(RpfHeader.HEADER_SECTION_LENGTH);
			fout.writeBytes("       A.TOC"); // has to be padded.
			fout.writeByte(head.neww);

			fout.writeBytes(head.standardNumber);
			if (head.standardNumber.length() < 15) {
				fout.writeBytes(createPadding(15 - head.standardNumber.length(), false));
			}

			fout.writeBytes(head.standardDate);
			if (head.standardDate.length() < 8) {
				fout.writeBytes(createPadding(8 - head.standardDate.length(), false));
			}

			// All this trouble just for a silly character.
			char[] charArray = new char[1];
			charArray[0] = head.classification;
			charString = new String(charArray);
			fout.writeBytes(charString);

			Debug.message("maketoc",
					"MakeToc: writing country(" + head.country + ") and release(" + head.release + ")");

			fout.writeBytes(head.country);
			fout.writeBytes(head.release);

			/*
			 * New, DKS. no longer head.loc_sec_phys_loc. Always write 48.
			 */
			/*
			 * DFD - This isn't true, but since we don't care about NITF
			 * formatting, it may be. Just write out where we are.
			 */
			int location_section_location = (int) fout.getFilePointer() + 4;
			fout.writeInt(location_section_location);

			if (Debug.debugging("maketoc")) {
				Debug.output("MakeToc: location section location is : " + location_section_location);
			}

			if (Debug.debugging("maketoc")) {
				Debug.output("MakeToc: *** writing location section ***");
			}

			/* LOCATION SECTION */
			int Loc_hdr_len = 14; /* Location section header length */
			int Loc_sec_comp_len = 10; /*
										 * Location section component length
										 */

			/* 14 + 4 * 10 = 54 */
			Loc_sec_len = Loc_hdr_len + (RpfFileSections.TOC_LOCATION_KEY * Loc_sec_comp_len);
			fout.writeShort(Loc_sec_len);
			/* compon. loc tbl offset: location section hdr length */
			fout.writeInt(Loc_hdr_len);
			/* # records in location section: 4 */
			fout.writeShort(RpfFileSections.TOC_LOCATION_KEY);
			/* component location record length: 10 */
			fout.writeShort(Loc_sec_comp_len);

			if (Debug.debugging("maketoc")) {
				Debug.output("MakeToc:\n  location section length: " + Loc_sec_len + "\n  location header length: "
						+ Loc_hdr_len + "\n  number of location records: " + RpfFileSections.TOC_LOCATION_KEY
						+ "\n  location section comp length: " + Loc_sec_comp_len);
			}

			/*
			 * compon. aggregate len: unknown here. Fill in after doing all
			 * else.
			 */
			/* location component aggregate length file location */
			long agg_loc = fout.getFilePointer(); /* save for later */
			fout.writeInt(0); // place holder.

			/* Begin: location section, component location table */

			int Bound_hdr_len = 8; /* Boundary section header length */
			int Bound_rec_len = 132; /* Boundary record length */

			/* Boundary section length */
			int Bound_sec_len = Bound_hdr_len + (groupCount * Bound_rec_len);

			/* Compute frame section length, for later */
			pathname_pos[0] = 0; /* cum. offset */
			int uniq_dir_cnt = 0; /* # of unique directory paths. */

			// Looking for the directory name for each file.
			for (i = 0; i < nFrames; i++) { /* for each frame file */

				/*
				 * set tail to ptr to last occurrence of '/' in filename
				 */
				/* frames[i].filename is full pathname */
				frame = (Frame) frames.elementAt(i);

				tail = frame.filename.lastIndexOf(File.separatorChar);
				if (tail == -1) {
					direct[i] = frame.filename;
				} else {
					// Java-cise the name, so it meets spec.
					direct[i] = frame.filename.substring(0, ++tail).replace('\\', '/');
				}

				if (Debug.debugging("maketocdetail"))
					Debug.output("MakeToc: Matching directory: " + direct[i]);

				/* Match direct. names with list of uniq direct. names */

				/* flag for found name in list */
				boolean uniq_dir_match = false;
				String tmpDir = null;

				if (relativeFramePaths) {
					int rpfIndex = direct[i].lastIndexOf("RPF");
					if (rpfIndex == -1) {
						rpfIndex = direct[i].lastIndexOf("rpf");
					}
					if (rpfIndex != -1) {
						rpfIndex += 3;
						if (direct[i].length() > rpfIndex && direct[i].charAt(rpfIndex) == '/') {

							rpfIndex++;
						}
						tmpDir = "./" + direct[i].substring(rpfIndex);
					} else {
						if (Debug.debugging("maketoc")) {
							Debug.output(
									"RPF directory not found in directory path " + direct[i] + ", using absolute path");
						}
						tmpDir = direct[i];
					}

				} else {
					tmpDir = direct[i];
				}

				for (j = 0; j < uniq_dir_cnt; j++) {
					if (tmpDir.equals(uniq_dir[j])) {
						uniq_dir_ptr[i] = j;
						uniq_dir_match = true;
						if (Debug.debugging("maketocdetail"))
							Debug.output("Found match with: " + uniq_dir[j]);
						break;
					}
				}

				if (!uniq_dir_match) {
					uniq_dir[uniq_dir_cnt] = tmpDir;
					uniq_dir_ptr[i] = uniq_dir_cnt;
					if (Debug.debugging("maketoc"))
						Debug.output("Adding Unique directory: " + uniq_dir[uniq_dir_cnt]);
					uniq_dir_cnt++;
				} /* if */

			} /* for i */

			if (Debug.debugging("maketoc"))
				Debug.output("Uniq_dir_cnt: " + uniq_dir_cnt);

			/* compute uniq dir pathname table length */
			int path_table_len = 0;
			for (j = 0; j < uniq_dir_cnt; j++) {
				/* 2 for path length var. in hdr */
				path_table_len += 2 + uniq_dir[j].length();
			} /* for j */

			/* compute directory name positions in file */
			uniq_dir_pos[0] = 0;
			for (j = 1; j < uniq_dir_cnt; j++) {
				uniq_dir_pos[j] = uniq_dir_pos[j - 1] + 2 + uniq_dir[j - 1].length();
			} /* for j */

			for (j = 0; j < uniq_dir_cnt; j++) {
				if (Debug.debugging("maketocdetail"))
					Debug.output("j: " + j + ", uniq_dir_pos[j]: " + uniq_dir_pos[j]);
			} /* for j */

			/* compute direct. positions for each input file pathname */
			for (i = 0; i < nFrames; i++) {
				pathname_pos[i] = uniq_dir_pos[uniq_dir_ptr[i]];
				if (Debug.debugging("maketocdetail"))
					Debug.output("i: " + i + ", pathname_pos[i]:" + pathname_pos[i]);
			} /* for i */

			/*
			 * frame file index record length: 9 + nFrames * 33 + path_table_len
			 */
			Frame_sec_len = Frame_hdr_len + nFrames * Frame_index_rec_len + path_table_len;

			/* START LOCATION RECORD 1 */
			/* ID #: */
			fout.writeShort((short) RpfFileSections.LOC_BOUNDARY_SECTION_SUBHEADER);

			// The boundary section subheader is the first part of the
			// boundary rectangle section. The boundary section comes
			// right after the header (in this program - the spec will
			// allow it to be anywhere).

			/* Boundary section subheader length */
			fout.writeInt(Bound_hdr_len);

			/* DKS. Physical location */
			/* 0 + 48 + 54 */
			fout.writeInt(TOC_Nitf_hdr_size + RpfHeader.HEADER_SECTION_LENGTH + Loc_sec_len);

			/* START LOCATION RECORD 2 */
			/* ID #: */
			fout.writeShort((short) RpfFileSections.LOC_BOUNDARY_RECTANGLE_TABLE);

			/* Boundary rectangle table length */
			Bound_tbl_len = groupCount * Bound_rec_len;
			fout.writeInt(Bound_tbl_len);

			/* DKS. Physical location */
			/* 0 + 48 + 54 + 8 */
			fout.writeInt(TOC_Nitf_hdr_size + RpfHeader.HEADER_SECTION_LENGTH + Loc_sec_len + Bound_hdr_len);

			Bound_sec_len = Bound_hdr_len + Bound_tbl_len;

			/* START LOCATION RECORD 3 */
			/* ID #: */
			fout.writeShort((short) RpfFileSections.LOC_FRAME_FILE_INDEX_SUBHEADER);

			/* length */
			fout.writeInt(Frame_hdr_len);

			/* physical index (offset) */
			fout.writeInt(TOC_Nitf_hdr_size + RpfHeader.HEADER_SECTION_LENGTH + Loc_sec_len + Bound_sec_len);

			/* START LOCATION RECORD 4 */
			/* ID #: */
			fout.writeShort((short) RpfFileSections.LOC_FRAME_FILE_INDEX_SUBSECTION);

			/* length */
			/* Frame_sec_len computed above */
			fout.writeInt(Frame_sec_len - Frame_hdr_len);

			/* physical index (offset) */
			fout.writeInt(
					TOC_Nitf_hdr_size + RpfHeader.HEADER_SECTION_LENGTH + Loc_sec_len + Bound_sec_len + Frame_hdr_len);

			if (Debug.debugging("maketoc")) {
				Debug.output("MakeToc: boundary section at : " + fout.getFilePointer());
			}

			/* START BOUNDARY RECTANGLE SECTION */
			if (Debug.debugging("maketoc")) {
				Debug.output("MakeToc: *** writing boundary rectangles ***");
			}

			/* Subheader */
			/* boundary rectangle table offset */
			fout.writeInt(0);

			/* # of boundary rectangle records */
			fout.writeShort((short) groupCount);

			/* boundary rectangle record length */
			fout.writeShort((short) Bound_rec_len);

			/* For each boundary rectangle record */
			for (i = 0; i < groupCount; i++) {

				group = (Group) groups.elementAt(i);

				/*
				 * Key off flag to write proper data to A.TOC for browse menu
				 * later
				 */
				if (group.cib) {
					fout.writeBytes("CIB  ");
					fout.writeBytes("8:1  "); /* Compr. ratio */
				} else if (group.cdted) {
					fout.writeBytes("CDTED");
					fout.writeBytes("6.5:1"); /*
												 * Compr. ratio: VARIABLE
												 */
				} else {
					fout.writeBytes("CADRG");
					fout.writeBytes("55:1 ");
				} /* else */

				// Should be 12 padded chars, check just in case...
				if (group.scale.length() < 12) {
					fout.writeBytes(group.scale);
					fout.writeBytes(createPadding(12 - group.scale.length(), false));
				} else {
					fout.writeBytes(group.scale.substring(0, 12)); // Already
					// 12
					// padded
					// chars
				}

				// All this trouble just for a silly character.
				charArray[0] = group.zone;
				charString = new String(charArray);
				fout.writeBytes(charString);

				/* DKS changed from AFESC to DMAAC 8/2/94 */
				/* Producer: */
				// Should be OpenMap BBN, I guess.
				fout.writeBytes(producer);

				/*
				 * PBF - If group is polar, change boundaries from rect
				 * coordinates to lat-lon -- 6-19-94
				 */
				if (group.zone == '9' || group.zone == 'J') { /*
																 * polar zone
																 */
					/*
					 * DKS: switched x,y to match spec: x increases right, y up.
					 */
					ytop = group.horiz_pos[group.top];
					ybottom = group.horiz_pos[group.bottom];
					xleft = group.vert_pos[group.left];
					xright = group.vert_pos[group.right];

					if (Debug.debugging("maketoc")) {
						Debug.output("POLAR ZONE. ytop: " + ytop + ", ybottom: " + ybottom + ", xleft: " + xleft
								+ ", xright:" + xright);
					}
					/* see CADRG SPEC 89038, p. 50 */
					/*
					 * FIND LATITUDES from x,y. x increases right, y up.
					 */

					/* DKS new 1/95 to handle South pole separately. */
					/* h_interval converts from pix to deg. */
					if (group.zone == '9') { /* "9": NORTH POLE */
						top = 90 - (Math.sqrt((ytop * ytop) + (xleft * xleft)) * (group.h_interval));
						bottom = 90 - (Math.sqrt((ybottom * ybottom) + (xright * xright)) * (group.h_interval));
					} /* North pole */

					else { /* "J": South Pole */
						top = -90 + (Math.sqrt((ytop * ytop) + (xleft * xleft)) * (group.h_interval));
						bottom = -90 + (Math.sqrt((ybottom * ybottom) + (xright * xright)) * (group.h_interval));
					} /* South pole */

					if (Debug.debugging("maketoc"))
						Debug.output("LATS. top: " + top + ", bottom: " + bottom);

					/*
					 * Cvt from x,y to LONGITUDE; from radians to degrees
					 */

					/* DKS added South pole case 1/95 */

					if (group.zone == '9') { /* "9": NORTH POLE */
						left_t = 180.0 / Math.PI * Math.acos(-ytop / Math.sqrt((ytop * ytop) + (xleft * xleft)));
						/* DKS fixed bug 1/95: from ytop to ybottom */
						left_b = 180.0 / Math.PI
								* Math.acos(-ybottom / Math.sqrt((ybottom * ybottom) + (xleft * xleft)));
						right_t = 180.0 / Math.PI * Math.acos(-ytop / Math.sqrt((ytop * ytop) + (xright * xright)));
						/* DKS fixed bug 1/95: from ytop to ybottom */
						right_b = 180.0 / Math.PI
								* Math.acos(-ybottom / Math.sqrt((ybottom * ybottom) + (xright * xright)));
					} /* if North pole */

					else { /* South Pole */
						left_t = 180.0 / Math.PI * Math.acos(ytop / Math.sqrt((ytop * ytop) + (xleft * xleft)));
						/* DKS fixed bug 1/95: from ytop to ybottom */
						left_b = 180.0 / Math.PI
								* Math.acos(ybottom / Math.sqrt((ybottom * ybottom) + (xleft * xleft)));
						right_t = 180.0 / Math.PI * Math.acos(ytop / Math.sqrt((ytop * ytop) + (xright * xright)));
						/* DKS fixed bug 1/95: from ytop to ybottom */
						right_b = 180.0 / Math.PI
								* Math.acos(ybottom / Math.sqrt((ybottom * ybottom) + (xright * xright)));
					} /* if South pole */

					/* For both poles: */
					if (xleft < 0) { /*
										 * left half of earth has negative
										 * longits
										 */
						left_t = -left_t;
						left_b = -left_b;
					}
					/* This will hardly ever happen: */
					if (xright < 0) { /*
										 * left half of earth has negative longs
										 */
						right_t = -right_t;
						right_b = -right_b;
					}

					if (Debug.debugging("maketoc"))
						Debug.output("LONGS. left_t: " + left_t + ", right_t: " + right_t);
					if (Debug.debugging("maketoc"))
						Debug.output("LONGS. left_b: " + left_b + ", right_b: " + right_b);

					// #if 0
					// /* !!!!!!!!!!!!!!!!!!! Fix to getlat [80,90],
					// longit. [-180,180] */
					// bottom = 80.0 ;
					// top = 90.0 ;
					// left = -180.0 ;
					// right = 180.0 ;
					// #endif

				} /* if polar zone */

				/* end DKS portion */
				/* end PBF cvt from xy to lat-long */

				else { /* non-polar zone */

					left_t = group.vert_pos[group.left];
					left_b = left_t;
					right_t = group.vert_pos[group.right];
					right_b = right_t;
					top = group.horiz_pos[group.top];
					bottom = group.horiz_pos[group.bottom];

				} /* else */

				// Debug.output("For RpfTocEntry, writing: \n top = "
				// + top +
				// "\n bottom = " + bottom + "\n left = " + left_t +
				// "\n right = " + right_t + "\n---------");

				// Writing all doubles

				fout.writeDouble(top);
				fout.writeDouble(left_t);
				fout.writeDouble(bottom);
				fout.writeDouble(left_b);
				fout.writeDouble(top);
				fout.writeDouble(right_t);
				fout.writeDouble(bottom);
				fout.writeDouble(right_b);

				fout.writeDouble(group.v_resolution);
				fout.writeDouble(group.h_resolution);
				fout.writeDouble(group.v_interval);
				fout.writeDouble(group.h_interval);

				/* # frames */
				fout.writeInt((int) (group.bottom - group.top));
				fout.writeInt((int) (group.right - group.left));
			}

			if (Debug.debugging("maketoc")) {
				Debug.output("MakeToc: *** writing frame section ***");
				Debug.output("MakeToc: started with a 'U'");
			}

			/* START FRAME SECTION */
			/* Now write frames */

			/* security classif */
			charArray[0] = 'U';
			charString = new String(charArray);
			fout.writeBytes(charString);

			/* frame file index tbl offset */
			fout.writeInt(0);

			/* # of frame file index records */
			fout.writeInt(nFrames);

			/* # of pathname (directory) records */
			/* DKS NEW: was nFrames: */
			fout.writeShort(uniq_dir_cnt);

			/* frame file index record length : 33 */
			fout.writeShort(Frame_index_rec_len);

			/* Frame file index subsection */
			for (i = 0; i < nFrames; i++) { /* for each frame file */
				frame = (Frame) frames.elementAt(i);
				group = (Group) groups.elementAt(frame.group);

				if (!frame.marked) {
					Debug.error(frame.filename + ": not in a boundary rect??");
					// continue;
				}

				/* NEW, DKS: +1 removed so range is [0,n]: */
				fout.writeShort(frame.group); /* Boundary rect. rec. # */

				/* Frame location ROW number */
				/*
				 * DKS. Changed from top to bottom to fix bug in Theron's frame
				 * numbering
				 */
				/*
				 * Should start numbering at BOTTOM (southern-most part) of
				 * group
				 */
				/* !!! Changed back so row num is never <= 0 */
				/* Alternative is bottom-y, not y-bottom. Try later */
				/*
				 * us = frames[i].y - groups[frames[i].group].bottom + 1;
				 */
				/* NEW, DKS: START AT 0, NOT 1: REMOVE "+ 1": */
				/* us = frames[i].y - groups[frames[i].group].top; */

				/*
				 * SMN The frames number are from the bottom left not top left
				 */
				us = (short) (group.bottom - frame.y - 1);

				if (Debug.debugging("maketocframedetail")) {
					Debug.output("iframe: " + i + ", frame.y: " + frame.y);
					Debug.output("frame.group: " + frame.group);
					Debug.output("group.bottom:" + group.bottom);
					Debug.output("group.top:" + group.top);
					Debug.output("frame row #:" + us);
				}

				fout.writeShort(us);

				/* Frame location Column number */
				/* NEW, DKS: START AT 0, NOT 1: REMOVE "+ 1": */
				fout.writeShort((short) (frame.x - group.left));

				/* pathname record offset: */
				/*
				 * DKS 11/10: Now w.r.t. frame file index table subsection
				 */
				/*
				 * ui = head.HEADER_SECTION_LENGTH + Loc_sec_len + Bound_sec_len
				 * + Frame_hdr_len + nFrames*Frame_index_rec_len +
				 * pathname_pos[i] ;
				 */
				fout.writeInt((int) (nFrames * Frame_index_rec_len + pathname_pos[i]));

				String framename;
				tail = frame.filename.lastIndexOf(File.separatorChar);
				if (tail == -1) {
					framename = frame.filename;
				} else {
					framename = frame.filename.substring(++tail);
				}
				if (framename.length() > 12) {
					Debug.error("MakeToc: encountered a frame name that's too long!\n" + framename);
					framename = framename.substring(0, 12);
				}

				/* frame file name */
				fout.writeBytes(framename);

				String seriesCode = framename.substring(9, 11);

				/* Check for Overview image: affects GEOREF */
				if (!seriesCode.equalsIgnoreCase("OV") && !seriesCode.equalsIgnoreCase("LG")
						&& !seriesCode.equalsIgnoreCase("OI")) {
					/* Not Overview or Lengend img */
					/* DKS 8/1/94: handle polar zone separately */
					if (frame.zone != '9'
							|| frame.zone != 'J') { /*
													 * polar zone
													 */
						georef = latlong2GEOREF(frame.swlat, frame.swlon);
					} else { /* not polar */
						georef = latlong2GEOREF(frame.bottom, frame.left);
					} /* else */

				} else { /* Overview image has no GEOREF */
					if (Debug.debugging("maketoc"))
						Debug.output("Overview image has no GEOREF");
					georef = "000000";
				} /* else */

				fout.writeBytes(georef);

				/* classification */
				// HACK - assumes unclassified data.
				fout.writeBytes(charString);

				fout.writeBytes(head.country);
				fout.writeBytes(head.release);
			} /* for i (each frame file) */

			Debug.message("maketoc", "MakeToc: *** writing directory section ***");

			/* Pathname table */
			/*
			 * Write UNIQUE pathnames: really Directory name, e.g.
			 * "./CENTRAL.USA/"
			 */
			for (j = 0; j < uniq_dir_cnt; j++) {
				/* DKS new */
				/*
				 * write pathname length. !!?? may be padded in front to align
				 * on word boundary!!??
				 */
				fout.writeShort((short) (uniq_dir[j].length()));

				/* pathname */
				fout.writeBytes(uniq_dir[j]);
			} /* for j (each uniq directory) */

			/* No color table index section */

			/*
			 * Go back and fill in component aggregate length in location
			 * section
			 */
			fout.seek(agg_loc);
			fout.writeInt((int) (Bound_sec_len + Frame_sec_len));

			fout.close();
			Debug.message("maketoc", "MakeToc: *** Normal end of make-toc ***");

		} catch (IOException ioe) {
			throw new MakeTocException(ioe.getMessage());
		}

	} /* main */

	/**
	 * Take the Vector of frames, and group them into boundary rectangles,
	 * represented by groups. If Dchum is present, all frames get placed in
	 * their own group.
	 * 
	 * @param frames
	 *            the frame Vector.
	 * @param groups
	 *            the group Vector.
	 * @param isDchum
	 *            flag to note if Dchum frames are present.
	 */
	protected void groupFrames(Vector<Frame> frames, Vector<Group> groups, boolean isDchum) throws MakeTocException {

		Frame frame;
		Group group;
		int groupCount;

		int nFrames = frames.size();
		Debug.message("maketoc", "MakeToc: *** grouping frames ***");

		/* For each frame file */
		for (int i = 0; i < nFrames; i++) {
			Debug.message("maketocdetail", "MakeToc: group addition, starting outer loop");

			// Assuming that the vector objects are in the same order
			// as initially loaded.
			frame = (Frame) frames.elementAt(i);

			if (!frame.marked) {
				groupCount = groups.size();

				group = new Group();

				group.left = maxSide / 2;
				group.right = group.left + 1;
				group.top = maxSide / 2;
				group.bottom = group.top + 1;

				group.horiz_pos[group.top] = frame.top;
				group.horiz_pos[group.bottom] = frame.bottom;
				group.vert_pos[group.left] = frame.left;
				group.vert_pos[group.right] = frame.right;

				group.h_interval = frame.h_interval;
				group.v_interval = frame.v_interval;
				group.h_resolution = frame.h_resolution;
				group.v_resolution = frame.v_resolution;

				group.scale = frame.scale;
				group.zone = frame.zone;
				group.cib = frame.cib;
				group.cdted = frame.cdted;

				frame.x = group.left;
				/*
				 * DKS. Changed from top to bottom to fix bug in Theron's frame
				 * numbering
				 */
				/*
				 * Should start numbering at BOTTOM (southern-most part) of
				 * group
				 */
				/* DKS. Switched back to fix row # <=0 bug */
				frame.y = group.top;
				frame.group = groupCount;
				frame.marked = true;

				Debug.message("maketocdetail", "Maketoc.groupFrames: created group " + groupCount + " for frame " + i
						+ ", - " + frame.filename + " checking other frames for neighbors");

				/*
				 * If Dchum, create 1 group for each file. No need for call to
				 * "add".
				 */
				if (!isDchum) {
					for (int j = 0; j < nFrames; j++) {
						if (i == j) {
							Debug.message("maketocdetail", "Maketoc.groupFrames: inner loop, i = j = " + i
									+ ", frame that created group added to group, expecting false return");
							continue;
						}
						Frame f = (Frame) frames.elementAt(j);
						if (addFrameToGroup(group, f, groupCount)) {
							Debug.message("maketocdetail",
									"Maketoc.groupFrames: added frame " + j + " to group " + groupCount);
							continue;
						}
					}
				}

				Debug.message("maketocdetail",
						"Maketoc.groupFrames: adding another group - " + groupCount + " *******************\n\n");

				groups.add(group);
			} /* if !frame.marked */

			fireProgressUpdate(ProgressEvent.UPDATE, "Organizing frames", i, nFrames);

		} /* for (i = 0; i < nFrames; i++) */

		if (Debug.debugging("maketoc")) {
			Debug.output("MakeToc: Number of boundary rectangles (groups): " + groups.size());
		}
	}

	/**
	 * Does the actual checking to see if the frame gets added to the group, by
	 * checking the frame's location with the group's current boundaries, and
	 * resizing the group boundary if the frame is touching it. Assumes
	 * everything has been allocated in the group and frame. Not prepared for
	 * either being null.
	 * 
	 * @param grp
	 *            the group
	 * @param frm
	 *            the frame.
	 * @param index
	 *            the group index, referring to it's position in the Group
	 *            Vector.
	 */
	protected boolean addFrameToGroup(Group grp, Frame frm, int index) throws MakeTocException {

		int i;
		int x;
		int y;

		if (frm.scale == null || grp.scale == null) {
			// This is a strange situation. The product codes in the file name
			// or header aren't resolving into a scale.

			// if these aren't set up properly, then other parameters aren't set
			// up properly either.
			Debug.output("grp and frm scale is null for " + frm.filename + ", skipping");
			return false;
		} else if (frm.marked || !frm.scale.equalsIgnoreCase(grp.scale) || frm.zone != grp.zone) {
			Debug.message("maketocframedetail",
					"\nMakeToc.addFrameToGroup: no action needed for frame, returning.\n  frm.marked = " + frm.marked
							+ "\n  frm.zone(" + frm.zone + ") = grp.zone(" + grp.zone + ")\n  frm.scale(" + frm.scale
							+ ") = grp.scale(" + grp.scale + ")\n");
			return false;
		}

		Debug.message("maketocframedetail", "MakeToc.addFrameToGroup: adding unmarked frame");

		double eps = frm.EPS();

		/* DKS. EPS TOLERANCE ADDED throughout */
		if (frm.left >= grp.vert_pos[grp.left] - eps && frm.right <= grp.vert_pos[grp.right] + eps
				&& frm.bottom >= grp.horiz_pos[grp.bottom] - eps && frm.top <= grp.horiz_pos[grp.top] + eps) {

			if (Debug.debugging("maketocdetail")) {
				Debug.output(frm.filename + " is in group " + index);
			}

		} else if (near(frm.right, grp.vert_pos[grp.left], eps) && frm.top <= grp.horiz_pos[grp.top] + eps
				&& frm.bottom >= grp.horiz_pos[grp.bottom] - eps) {

			if (Debug.debugging("maketocdetail")) {
				Debug.output(frm.filename + " add frame to group " + index + ": left side");
			}

			if (grp.left == 0) {
				throw new MakeTocException(
						"Boundary rectangle too small - Increase the boundary size to be larger than " + maxSide);
			}

			grp.left--; /* add to left side */
			grp.vert_pos[grp.left] = frm.left;
		} else if (near(frm.left, grp.vert_pos[grp.right], eps) && frm.top <= grp.horiz_pos[grp.top] + eps
				&& frm.bottom >= grp.horiz_pos[grp.bottom] - eps) {

			if (Debug.debugging("maketocdetail")) {
				Debug.output(frm.filename + ":add frame to group " + index + ": right side");
			}

			if (grp.right == maxSide) {
				throw new MakeTocException(
						"Boundary rectangle too small - Increase the boundary size to be larger than " + maxSide);
			}

			grp.vert_pos[grp.right] = frm.left;
			grp.right++; /* add to right */
			grp.vert_pos[grp.right] = frm.right;

		} else if (near(frm.bottom, grp.horiz_pos[grp.top], eps) && frm.right <= grp.vert_pos[grp.right] + eps
				&& frm.left >= grp.vert_pos[grp.left] - eps) {

			if (Debug.debugging("maketocdetail")) {
				Debug.output(frm.filename + ":add frame to group " + index + ": top");
			}

			if (grp.top == 0) {
				throw new MakeTocException(
						"Boundary rectangle too small - Increase the boundary size to be larger than " + maxSide);
			}

			grp.top--; /* add to top */
			grp.horiz_pos[grp.top] = frm.top;
		} else if (near(frm.top, grp.horiz_pos[grp.bottom], eps) && frm.right <= grp.vert_pos[grp.right] + eps
				&& frm.left >= grp.vert_pos[grp.left] - eps) {

			if (Debug.debugging("maketocdetail")) {
				Debug.output(frm.filename + ":add frame to group " + index + ": bottom");
			}

			if (grp.bottom == maxSide) {
				throw new MakeTocException(
						"Boundary rectangle too small - Increase the boundary size to be larger than " + maxSide);
			}

			grp.horiz_pos[grp.bottom] = frm.top;
			grp.bottom++; /* add to bottom */
			grp.horiz_pos[grp.bottom] = frm.bottom;
		} else {
			Debug.message("maketocframedetail",
					"MakeToc.add: frame not close enough to anything else, not adding to group.");
			return false;
		}

		x = y = -1;
		for (i = grp.left; i < grp.right; i++) {
			/*
			 * PBF - Change from (==) to near function for polar 6-19-94
			 */
			if (near(frm.left, grp.vert_pos[i], eps)) {
				x = i;
				break;
			}
		}

		for (i = grp.top; i < grp.bottom; i++) {
			/*
			 * PBF - Change from (==) to near function for polar 6-19-94
			 */
			if (near(frm.top, grp.horiz_pos[i], eps)) {
				y = i;
				break;
			}
		}

		if (x < 0 || y < 0) {
			Debug.output("MakeToc: " + frm.filename + ": in rect but can't find boundary (horizontal"
					+ (x < 0 ? " bad" : " OK") + ", vertical" + (y < 0 ? " bad)" : " OK)"));

			if (Debug.debugging("maketocframedetail")) {

				Debug.output(" - For frame: \n  " + frm.toString());

				Debug.output(" - Group horizontal left: " + grp.left + " vs. right: " + grp.right);

				for (i = grp.left; i < grp.right; i++) {
					/*
					 * PBF - Change from (==) to near function for polar 6-19-94
					 */
					Debug.output(" - Checking horizontal: " + frm.left + " <-> " + grp.vert_pos[i]);
					if (near(frm.left, grp.vert_pos[i], eps)) {
						Debug.output(" Last one should have hit.");
					}
				}

				Debug.output(" - Group vertical top: " + grp.horiz_pos[grp.top] + " vs. bottom: "
						+ grp.horiz_pos[grp.bottom] + ", frame top = " + frm.top + " and frame bottom = " + frm.bottom);

				for (i = grp.top; i < grp.bottom; i++) {
					/*
					 * PBF - Change from (==) to near function for polar 6-19-94
					 */
					Debug.output(" - Checking vertical: " + frm.top + " <-> " + grp.horiz_pos[i]);
					if (near(frm.top, grp.horiz_pos[i], eps)) {
						Debug.output(" Last one should have hit.");
					}
				}
			}

			throw new MakeTocException(frm.filename + " in rect but can't find boundary (horizontal"
					+ (x < 0 ? " bad" : " OK") + ", vertical" + (y < 0 ? " bad)" : " OK)"));
		}

		/* DKS ABS, frm.EPS2 added */
		/*
		 * DKS 8/16/94: h_resolution (meters/pix) will vary from frame to frame
		 * NS
		 */
		/* Therefore don't check for a match here */
		if (Math.abs(frm.h_interval - grp.h_interval) > EPS2 || Math
				.abs(frm.v_interval - grp.v_interval) > EPS2) /*
																 * deg / pix
																 */
		/*
		 * Math.abs (frm.h_resolution - grp.h_resolution) > EPS2 || Math.abs
		 * (frm.v_resolution - grp.v_resolution) > EPS2)
		 */
		{
			Debug.error(frm.filename + ": interval mismatch\n  frm.h_interval: " + frm.h_interval + ", grp.h_interval:"
					+ grp.h_interval + "\n  frm.v_interval: " + frm.v_interval + ", grp.v_interval: " + grp.v_interval
					+ "\n  frm.h_resolution: " + frm.h_resolution + ", grp.h_resolution: " + grp.h_resolution
					+ "\n  frm.h_resolution: " + frm.h_resolution + ", grp.h_resolution: " + grp.h_resolution);
			throw new MakeTocException(frm.filename + " has mismatched frame resolution");
		}

		frm.marked = true;
		frm.group = index;
		frm.x = x;
		frm.y = y;
		grp.cib = frm.cib;
		grp.cdted = frm.cdted;
		return true;
	} /* add */

	/**
	 * This program attempts to convert latitudes and longitudes given in a
	 * decimal format into a GEOREF alphanumeric designation code. The first
	 * letter of the code denotes the longitudinal 15 degree grid that contains
	 * the area of interest. The second letter denotes the latitudinal 15 degree
	 * grid. The third letter denotes the one degree longitudinal grid within
	 * the 15 degree longitudinal grid. The fourth letter denotes the one degree
	 * latitudinal grid within the 15 degree latitudinal grid. The fifth
	 * character is a number denoting the minutes longitudinally to the nearest
	 * 10. The sixth number denotes the minutes latitudinally to the nearest 10.
	 * Wouldn't it just have been easier to use the decimal latitudes and
	 * longitudes?
	 */
	protected String latlong2GEOREF(double latitude, double longitude) {
		int i;
		char tmp = 'A'; // no reason for 'A'
		char tmp1 = 'A';
		char tmp2 = 'A';

		// These serve as tmps in integer form.
		int tmpi, tmpi1, tmpi2;

		/*
		 * this portion of the code calculates the longitudinal part of the
		 */
		/*
		 * GEOREF number. I can't explain the logic -- I don't understand
		 */
		/* how it works. All that I know is that it seems to. */
		LatLonPoint llp = new LatLonPoint.Double(latitude, longitude);
		DMSLatLonPoint dmsp = new DMSLatLonPoint(llp);

		char[] GEOSTRING = new char[6];

		if (longitude == 0.0000) {
			tmp = 'N';
			tmp1 = 'A';
			tmp2 = '0';
		} else if (longitude == -180.0000) {
			tmp = 'A';
			tmp1 = 'A';
			tmp2 = '0';
		} else if (longitude == 180.0000) {
			tmp = 'Z';
			tmp1 = 'Q';
			tmp2 = '9';
		} else if (longitude > 0.0000) {
			tmpi = dmsp.lon_degrees / 15;
			tmpi += 78;
			if (tmpi >= 79) {
				tmpi += 1;
			}
			if (tmpi > 90) {
				tmpi = 90;
			}
			tmp = (char) tmpi;

			// Setting i to a certain value, based on longitude.
			for (i = 0; i * 15 < (int) (longitude + 0.9999); i++) {
			}
			tmpi1 = 15 * i - (int) (longitude);
			if ((tmpi1 >= 3) && (tmpi1 < 8)) {
				tmpi1 += 1;
			} else if (tmpi1 >= 8) {
				tmpi1 += 2;
			}

			if (tmpi1 != 0) {
				tmpi1 = 82 - tmpi1;
				tmp1 = (char) tmpi1;
			} else {
				tmp1 = 'A';
			}
			if (tmp1 == 'R') {
				tmp1 = 'A';
			}
			tmpi2 = (int) ('0') + (dmsp.lon_minutes / 10);
			tmp2 = (char) tmpi2;

		} else if (longitude <= 0.0000) {

			tmpi = (int) (((double) dmsp.lon_degrees) / 15.0 - 0.999);
			tmpi = 77 - Math.abs(tmpi);
			if (tmpi >= 73) {
				tmpi += 1;
			}
			if (tmpi > 77) {
				tmpi = 77;
			}
			tmp = (char) tmpi;

			/* DKS changed from abs to fabs */
			for (i = 0; i * 15 < (int) (Math.abs((longitude - 0.9999))); i++) {
			}
			/* DKS changed from abs to fabs */
			tmpi1 = i * 15 - (int) (Math.abs((longitude - 0.9999)));
			if ((tmpi1 >= 8) && (tmpi1 < 13)) {
				tmpi1 += 1;
			} else if (tmpi1 >= 13) {
				tmpi1 += 2;
			}
			if (tmpi1 > 16) {
				tmpi1 = 16;
			}
			tmpi1 += 65;
			tmp1 = (char) tmpi1;

			if ((int) (dmsp.lon_minutes / 10) != 0) {
				tmpi2 = ((int) '0') + (6 - (int) (dmsp.lon_minutes / 10));
				tmp2 = (char) tmpi2;
			} else {
				tmp2 = '0';
			}
		}

		GEOSTRING[0] = tmp;
		GEOSTRING[2] = tmp1;
		GEOSTRING[4] = tmp2;

		/*
		 * this portion of the code calculates the latitudinal part of the
		 */
		/*
		 * GEOREF number. I can't explain the logic -- I don't understand
		 */
		/* how it works. All that I know is that it seems to. */

		if (latitude == 0.0000) {
			tmp = 'G';
			tmp1 = 'A';
			tmp2 = '0';
		} else if (latitude == 90.0000) {
			tmp = 'M';
			tmp1 = 'Q';
			tmp2 = '9';
		} else if (latitude == -90.0000) {
			tmp = 'A';
			tmp1 = 'A';
			tmp2 = '0';
		} else if (latitude > 0.0000) {
			tmpi = dmsp.lat_degrees / 15;
			tmpi += 71;
			if (tmpi >= 73) {
				tmpi += 1;
			}
			if (tmpi > 77) {
				tmpi = 77;
			}
			tmp = (char) tmpi;

			for (i = 0; i * 15 < (int) (latitude + 0.9999); i++) {
			}
			tmpi1 = 15 * i - (int) (latitude);
			if ((tmpi1 >= 3) && (tmpi1 < 8)) {
				tmpi1 += 1;
			} else if (tmpi1 >= 8) {
				tmpi1 += 2;
			}
			tmpi1 = 82 - tmpi1;
			tmp1 = (char) tmpi1;

			if (tmp1 == 'R') {
				tmp1 = 'A';
			}
			if ((dmsp.lat_minutes / 10) != 0) {
				tmpi2 = ((int) '0') + (int) (dmsp.lat_minutes / 10);
				tmp2 = (char) tmpi2;
			} else {
				tmp2 = '0';
			}

		} else if (latitude < 0.0000) {

			tmpi = (int) ((double) dmsp.lat_degrees / 15.0 - 0.999);
			tmpi = 71 - Math.abs(tmpi);
			if (tmpi < 65) {
				tmpi = 65;
			}
			/* DKS changed from abs to fabs */
			for (i = 0; i * 15 < (int) (Math.abs((latitude - 0.9999))); i++) {
			}
			/* DKS changed from abs to fabs */
			tmpi1 = i * 15 - (int) (Math.abs((latitude - 0.9999)));
			if ((tmpi1 >= 8) && (tmpi1 < 13)) {
				tmpi1 += 1;
			} else if (tmpi1 >= 13) {
				tmpi1 += 2;
			}
			if (tmpi1 > 16) {
				tmpi1 = 16;
			}
			tmpi1 = 65 + tmpi1;
			tmp1 = (char) tmpi1;

			tmpi2 = ((int) '0') + (6 - (int) (dmsp.lat_minutes / 10));
			tmp2 = (char) tmpi2;
		}

		GEOSTRING[1] = tmp;
		GEOSTRING[3] = tmp1;
		GEOSTRING[5] = tmp2;

		String ret = new String(GEOSTRING);
		if (Debug.debugging("maketocdetail")) {
			Debug.output("latlon2GEOREF: lat = " + latitude + ", lon = " + longitude + ", GEOREF = " + ret);
		}

		return ret;
	} /* latlong2GEOREF() */

	public String createPadding(int length, boolean nullTerminated) {
		StringBuffer sb = new StringBuffer();
		for (int i = 0; i < length; i++) {
			if (i == length - 1 && nullTerminated) {
				sb.append("/0");
			} else {
				sb.append(" ");
			}
		}
		return sb.toString();
	}

	/**
	 * Add a ProgressListener that will display build progress.
	 */
	public void addProgressListener(ProgressListener list) {
		progressSupport.add(list);
	}

	/**
	 * Remove a ProgressListener that displayed build progress.
	 */
	public void removeProgressListener(ProgressListener list) {
		progressSupport.remove(list);
	}

	/**
	 * Clear all progress listeners.
	 */
	public void clearProgressListeners() {
		progressSupport.clear();
	}

	/**
	 * Fire an build update to progress listeners.
	 * 
	 * @param frameNumber
	 *            the current frame count
	 * @param totalFrames
	 *            the total number of frames.
	 */
	protected void fireProgressUpdate(int type, String task, int frameNumber, int totalFrames) {
		progressSupport.fireUpdate(type, task, totalFrames, frameNumber);
	}
}